计划

第 1 周:基础架构 + 微服务搭建

学习目标: 项目脚手架、Nacos、数据库设计、基础 CRUD

核心任务:

1. 创建 Maven 多模块项目
2. 集成 Spring Cloud 框架
3. 设计核心数据库表(用户、创意、拍卖、交易等)
4. 实现用户服务和认证模块
5. 所有微服务注册到 Nacos
6. 配置 API Gateway

交付物:

  • ✅ 完整的多模块项目结构
  • ✅ 用户注册/登录/认证功能
  • ✅ 所有服务能通过 Nacos 发现

第 2 周:创意服务 + 缓存优化

学习目标: Redis 多级缓存、布隆过滤器、ElasticSearch 搜索

核心任务:

1. 实现创意作品的 CRUD(发布、编辑、删除)
2. 集成 Redis + Caffeine 多级缓存
   - 热门创意缓存
   - 搜索结果缓存
3. 布隆过滤器防止缓存穿透
4. ElasticSearch 全文搜索集成
5. 缓存预热任务

重点代码示例:

// 创意详情查询 - 三层缓存防护
public Artwork getArtworkDetail(Long artworkId) {
    // 第1层:本地缓存(Caffeine)
    Artwork cached = localCache.getIfPresent(artworkId);
    if (cached != null) {
        return cached;
    }

    // 第2层:布隆过滤器检查数据是否存在
    if (!bloomFilter.contains(artworkId)) {
        // 数据不存在,直接返回 null
        return null;
    }

    // 第3层:Redis 分布式缓存
    String key = "artwork:" + artworkId;
    Artwork redisData = redisTemplate.opsForValue()
        .get(key);
    if (redisData != null) {
        localCache.put(artworkId, redisData);
        return redisData;
    }

    // 都没有 → 查数据库(加分布式锁防击穿)
    RLock lock = redisson.getLock("artwork:lock:" + artworkId);
    try {
        if (!lock.tryLock(2, 10, TimeUnit.SECONDS)) {
            // 加锁失败,等待后重试
            Thread.sleep(50);
            return getArtworkDetail(artworkId);
        }

        Artwork dbData = artworkMapper.selectById(artworkId);
        if (dbData != null) {
            redisTemplate.opsForValue()
                .set(key, dbData, Duration.ofHours(1));
            localCache.put(artworkId, dbData);
        }
        return dbData;
    } finally {
        lock.unlock();
    }
}

交付物:

  • ✅ 创意作品完整的 CRUD
  • ✅ 缓存系统防穿透、击穿、雪崩
  • ✅ 搜索功能可用

第 3 周:消息队列 + 异步处理

学习目标: RabbitMQ、死信队列、消息可靠性、幂等性

核心任务:

1. 设计 MQ 消息体(事件驱动架构)
   - 创意发布事件
   - 拍卖结算事件
   - 交易完成事件
2. 实现消息发送者和消费者
3. 配置死信队列处理异常
4. 实现幂等性处理(重复消息)
5. 消息可靠性保证(确认机制)

核心事件设计:

// 事件类型定义
enum EventType {
    ARTWORK_PUBLISHED,      // 创意发布
    ARTWORK_REPORTED,       // 创意举报
    AUCTION_STARTED,        // 拍卖开始
    BID_PLACED,            // 出价
    AUCTION_ENDED,         // 拍卖结束
    TRANSACTION_COMPLETED, // 交易完成
    PAYMENT_SUCCESS,       // 支付成功
    REVENUE_SETTLED,       // 收益结算
    BLINDBOX_OPENED        // 盲盒打开
}

// 事件消息
@Data
public class DomainEvent {
    private String eventId;        // 幂等性 key
    private EventType eventType;
    private Long timestamp;
    private Map<String, Object> payload;
    private String source;         // 来源服务
}

交付物:

  • ✅ MQ 系统完整接入
  • ✅ 创意发布 → 通知用户 的异步流程
  • ✅ 消息可靠性测试通过

第 4 周:拍卖核心逻辑 + 实时竞价

学习目标: WebSocket、Sentinel 限流、复杂业务逻辑、Redis Lua 脚本

核心任务:

1. 设计拍卖数据模型和状态机
2. 实现三种拍卖策略
   - 英式拍卖(逐步递增)
   - 荷兰式拍卖(逐步递减)
   - 密封竞价(一次性出价)
3. WebSocket 实时推送竞价信息
4. Sentinel 防止恶意出价(限流)
5. Redis Lua 脚本实现原子出价操作
6. 竞价锁防止并发冲突

Lua 脚本示例:

-- bid_lock.lua
-- 原子操作:检查出价有效性 + 记录出价 + 更新最高价

local auctionId = KEYS[1]
local userId = KEYS[2]
local bidAmount = ARGV[1]
local currentMaxBid = redis.call('GET', 'auction:' .. auctionId .. ':max_bid')

-- 检查出价是否有效
if tonumber(bidAmount) <= tonumber(currentMaxBid or 0) then
    return {0, "出价必须高于当前最高价"}
end

-- 检查用户余额(从钱包服务缓存)
local userBalance = redis.call('GET', 'wallet:' .. userId .. ':balance')
if tonumber(userBalance or 0) < tonumber(bidAmount) then
    return {0, "余额不足"}
end

-- 原子更新最高价和最高出价者
redis.call('SET', 'auction:' .. auctionId .. ':max_bid', bidAmount)
redis.call('SET', 'auction:' .. auctionId .. ':max_bidder', userId)
redis.call('LPUSH', 'auction:' .. auctionId .. ':bid_history',
    userId .. ':' .. bidAmount .. ':' .. redis.call('TIME')[1])

-- 发布出价事件到 pub/sub(WebSocket 订阅)
redis.call('PUBLISH', 'auction:' .. auctionId .. ':bids',
    cjson.encode({userId, bidAmount, redis.call('TIME')[1]}))

return {1, "出价成功"}

WebSocket 推送示例:

@ServerEndpoint("/ws/auction/{auctionId}")
public class AuctionWebSocketServer {

    private static Set<Session> sessions = new CopyOnWriteArraySet<>();
    private RedisTemplate<String, Object> redisTemplate;

    @OnOpen
    public void onOpen(@PathParam("auctionId") Long auctionId,
                      Session session) {
        sessions.add(session);
        // 订阅 Redis Pub/Sub 频道
        subscribeToAuctionChannel(auctionId);
    }

    private void subscribeToAuctionChannel(Long auctionId) {
        String channel = "auction:" + auctionId + ":bids";
        redisTemplate.getConnectionFactory()
            .getConnection()
            .subscribe((message, pattern) -> {
                // 实时广播给所有连接的客户端
                String bidInfo = new String(message.getBody());
                sessions.forEach(session -> {
                    try {
                        session.getBasicRemote()
                            .sendText(bidInfo);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
            }, channel.getBytes());
    }
}

交付物:

  • ✅ 拍卖系统完整可用
  • ✅ WebSocket 实时竞价
  • ✅ 支持多种拍卖策略
  • ✅ 并发安全性测试通过(万级并发)

第 5 周:盲盒 + 聚合拍卖 + 订单系统

学习目标: 复杂业务逻辑、抽奖算法、组合交易

核心任务:

1. 盲盒系统
   - 盲盒创建和库存管理
   - 奖品分布策略(概率分层)
   - 开箱动画数据和结果
   - 防止重复开箱(幂等性)

2. 聚合拍卖(组合购买)
   - 选择多个创意组合
   - 一次竞价多个作品
   - 批量结算

3. 订单系统
   - 订单创建、支付、确认
   - 订单查询和历史记录
   - 订单取消和退款

盲盒抽奖算法:

@Service
public class LotteryService {

    private static final Random random = new Random();

    // 加权随机抽奖
    public Prize drawPrize(BlindBox blindBox) {
        List<Prize> prizes = blindBox.getPrizes();

        // 计算权重总和
        int totalWeight = prizes.stream()
            .mapToInt(Prize::getWeight)
            .sum();

        // 随机选择
        int randomValue = random.nextInt(totalWeight);
        int currentWeight = 0;

        for (Prize prize : prizes) {
            currentWeight += prize.getWeight();
            if (randomValue < currentWeight) {
                return prize;
            }
        }

        return prizes.get(0); // 默认返回
    }

    // 开箱逻辑(包含防重复操作)
    @Transactional
    public BoxOpenResult openBox(String userId, Long boxId) {
        // Redis 中记录用户是否已开过这个盒子
        String openKey = "blindbox:opened:" + userId + ":" + boxId;

        // 使用 Lua 脚本保证原子性
        Boolean alreadyOpened = redisTemplate.execute(
            new DefaultRedisScript<>(
                "if redis.call('get', KEYS[1]) then " +
                "  return 1 " +
                "else " +
                "  redis.call('set', KEYS[1], '1') " +
                "  redis.call('expire', KEYS[1], 86400) " +
                "  return 0 " +
                "end",
                Boolean.class
            ),
            Collections.singletonList(openKey)
        );

        if (alreadyOpened) {
            throw new BusinessException("已经开过这个盲盒了");
        }

        // 真正的抽奖逻辑
        BlindBox box = blindBoxService.getById(boxId);
        Prize prize = drawPrize(box);

        // 发送 MQ 事件
        eventPublisher.publish(new BoxOpenedEvent(
            userId, boxId, prize.getId()
        ));

        return BoxOpenResult.builder()
            .prizeId(prize.getId())
            .prizeName(prize.getName())
            .prizeImage(prize.getImage())
            .rarity(prize.getRarity())  // 稀有度
            .animationDelay(800)        // 动画延迟(毫秒)
            .build();
    }
}

聚合拍卖逻辑:

@Service
public class CombinedAuctionService {

    // 创建组合拍卖
    @Transactional
    public CombinedAuction createCombinedAuction(
            List<Long> artworkIds,
            BigDecimal startPrice) {

        // 验证所有作品都存在
        List<Artwork> artworks = artworkService.listByIds(artworkIds);
        if (artworks.size() != artworkIds.size()) {
            throw new BusinessException("部分作品不存在");
        }

        // 创建组合拍卖记录
        CombinedAuction auction = new CombinedAuction();
        auction.setArtworks(artworks);
        auction.setStartPrice(startPrice);
        auction.setStatus(AuctionStatus.PENDING);
        auction.setCreatedTime(LocalDateTime.now());

        combinedAuctionMapper.insert(auction);

        // 发送事件
        eventPublisher.publish(new AuctionCreatedEvent(auction.getId()));

        return auction;
    }

    // 组合拍卖出价
    @Transactional
    public void bidOnCombination(Long auctionId,
                                 Long userId,
                                 BigDecimal bidAmount) {

        CombinedAuction auction = combinedAuctionMapper
            .selectById(auctionId);

        // 检查出价金额
        if (bidAmount.compareTo(auction.getCurrentPrice()) <= 0) {
            throw new BusinessException("出价必须高于当前价格");
        }

        // 原子操作:更新价格 + 记录出价
        // 使用 Lua 脚本
        String lua = "redis.call('set', KEYS[1], ARGV[1]) " +
                     "redis.call('set', KEYS[2], ARGV[2]) " +
                     "return 1";

        redisTemplate.execute(
            new DefaultRedisScript<>(lua, Long.class),
            Arrays.asList(
                "auction:price:" + auctionId,
                "auction:bidder:" + auctionId
            ),
            bidAmount.toPlainString(),
            userId.toString()
        );

        // 发布竞价事件
        eventPublisher.publish(
            new CombinedBidPlacedEvent(auctionId, userId, bidAmount)
        );
    }
}

订单系统:

@Service
public class OrderService {

    // 创建订单(可能来自多种场景:拍卖结束、盲盒、直接购买)
    @Transactional
    public Order createOrder(OrderCreateRequest request) {
        Order order = new Order();
        order.setUserId(request.getUserId());
        order.setTotalAmount(request.getTotalAmount());
        order.setStatus(OrderStatus.CREATED);
        order.setItems(request.getItems());
        order.setCreatedTime(LocalDateTime.now());

        orderMapper.insert(order);

        // 发送订单创建事件
        eventPublisher.publish(new OrderCreatedEvent(order.getId()));

        return order;
    }

    // 订单支付(通过钱包扣费)
    @Transactional
    public void payOrder(Long orderId) {
        Order order = orderMapper.selectById(orderId);

        // 调用钱包服务扣费(Feign RPC)
        walletServiceClient.deductBalance(
            order.getUserId(),
            order.getTotalAmount()
        );

        // 更新订单状态
        order.setStatus(OrderStatus.PAID);
        order.setPaidTime(LocalDateTime.now());
        orderMapper.updateById(order);

        // 发送支付成功事件
        eventPublisher.publish(new PaymentSuccessEvent(order.getId()));
    }
}

交付物:

  • ✅ 盲盒系统完整实现(防重复、概率分层)
  • ✅ 聚合拍卖支持
  • ✅ 订单系统 CRUD

第 6 周:钱包系统 + 分布式事务

学习目标: Seata 分布式事务、TCC 模式、补偿逻辑

核心任务:

1. 钱包系统
   - 用户余额管理
   - 充值和提现
   - 余额锁定(下单时冻结金额)

2. Seata 分布式事务
   - AT 模式:自动回滚
   - TCC 模式:业务补偿
   - SAGA 模式:长事务编排

3. 交易结算
   - 创作者收益分成
   - 平台抽成
   - 税费计算

钱包服务实现:

@Service
@Slf4j
public class WalletService {

    // 用户余额扣费(使用 Redis 实现)
    public void deductBalance(Long userId, BigDecimal amount) {
        String balanceKey = "wallet:balance:" + userId;
        String lockedKey = "wallet:locked:" + userId;

        // 使用 Lua 脚本保证原子性
        String luaScript =
            "local balance = tonumber(redis.call('GET', KEYS[1])) or 0 " +
            "local amount = tonumber(ARGV[1]) " +
            "if balance >= amount then " +
            "  redis.call('DECRBY', KEYS[1], amount) " +
            "  return 1 " +
            "else " +
            "  return 0 " +
            "end";

        Boolean result = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Boolean.class),
            Collections.singletonList(balanceKey),
            amount.toPlainString()
        );

        if (!result) {
            throw new InsufficientBalanceException("余额不足");
        }
    }

    // 余额锁定(下单时冻结)
    public void lockBalance(Long userId, BigDecimal amount) {
        String lockedKey = "wallet:locked:" + userId;
        redisTemplate.opsForValue()
            .increment(lockedKey, amount.doubleValue());
    }

    // 释放锁定的余额
    public void unlockBalance(Long userId, BigDecimal amount) {
        String lockedKey = "wallet:locked:" + userId;
        redisTemplate.opsForValue()
            .decrement(lockedKey, amount.doubleValue());
    }
}

Seata 分布式事务示例(TCC 模式):

// 业务接口
public interface OrderTransactionService {
    void createOrder(Long userId, Long artworkId, BigDecimal price);
}

// 实现类(带 Seata 注解)
@Service
@Slf4j
public class OrderTransactionServiceImpl
        implements OrderTransactionService {

    @Autowired
    private WalletServiceClient walletServiceClient;

    @Autowired
    private ArtworkServiceClient artworkServiceClient;

    @Autowired
    private OrderMapper orderMapper;

    // 全局事务开始
    @GlobalTransactional(name = "create-order-tx")
    @Override
    public void createOrder(Long userId, Long artworkId,
                           BigDecimal price) {

        log.info("开始创建订单:userId={}, artworkId={}",
            userId, artworkId);

        // Step 1: Try - 预留钱包余额
        walletServiceClient.lockBalance(userId, price);

        // Step 2: 创建订单记录
        Order order = new Order();
        order.setUserId(userId);
        order.setArtworkId(artworkId);
        order.setPrice(price);
        order.setStatus(OrderStatus.LOCKED);
        orderMapper.insert(order);

        // Step 3: 更新创意所有者的未结算收入
        artworkServiceClient.addUnsettledRevenue(
            artworkId, price
        );

        // 若任何一步失败,Seata 会自动回滚所有操作
        log.info("订单创建成功:orderId={}", order.getId());
    }
}

// 钱包服务(Seata 参与方)
@FeignClient(name = "wallet-service")
public interface WalletServiceClient {

    @PostMapping("/wallet/lock-balance")
    void lockBalance(@RequestParam Long userId,
                     @RequestParam BigDecimal amount);

    // Seata 回滚方法
    @PostMapping("/wallet/lock-balance-cancel")
    void lockBalanceCancel(@RequestParam Long userId,
                           @RequestParam BigDecimal amount);
}

// 实际钱包服务实现
@Service
public class WalletServiceImpl implements WalletService {

    @TwoPhaseBusinessAction(
        name = "lockBalance",
        commitMethod = "commit",
        rollbackMethod = "rollback"
    )
    public void lockBalance(String xid, Long branchId,
                           Long userId, BigDecimal amount) {
        // Try 阶段:锁定余额
        walletRepository.lockBalance(userId, amount);
        log.info("锁定余额成功:xid={}, userId={}, amount={}",
            xid, userId, amount);
    }

    // Commit 阶段:确认(无需操作,资金已锁定)
    public boolean commit(BusinessActionContext context) {
        return true;
    }

    // Rollback 阶段:回滚(释放锁定的余额)
    public boolean rollback(BusinessActionContext context) {
        Long userId = (Long) context.getActionContext("userId");
        BigDecimal amount =
            (BigDecimal) context.getActionContext("amount");

        walletRepository.unlockBalance(userId, amount);
        log.warn("回滚锁定余额:userId={}, amount={}",
            userId, amount);
        return true;
    }
}

收益结算逻辑:

@Service
@Slf4j
public class RevenueSettlementService {

    // 结算流程:创作者收益 = 交易金额 * (1 - 平台抽成)
    @Transactional
    public void settleRevenue(Long artworkId) {

        Artwork artwork = artworkMapper.selectById(artworkId);
        BigDecimal unsettledRevenue = artwork.getUnsettledRevenue();

        if (unsettledRevenue.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }

        // 计算创作者收益
        BigDecimal platformCommission = unsettledRevenue
            .multiply(PLATFORM_COMMISSION_RATE);  // 比如 10%
        BigDecimal creatorRevenue = unsettledRevenue
            .subtract(platformCommission);

        // 开始分布式事务:结算
        settleToCreator(artwork.getCreatorId(), creatorRevenue);
        settleToPlatform(platformCommission);

        // 更新创意的收益统计
        artwork.setSettledRevenue(
            artwork.getSettledRevenue().add(creatorRevenue)
        );
        artwork.setUnsettledRevenue(BigDecimal.ZERO);
        artworkMapper.updateById(artwork);

        log.info("结算完成:artworkId={}, creatorRevenue={}, " +
            "platformCommission={}", artworkId, creatorRevenue,
            platformCommission);
    }

    private void settleToCreator(Long creatorId,
                                BigDecimal amount) {
        // 发送 MQ 消息:创作者提现
        eventPublisher.publish(
            new CreatorRevenueReadyEvent(creatorId, amount)
        );
    }

    private void settleToPlatform(BigDecimal commission) {
        // 更新平台账户
        platformAccountRepository.addCommission(commission);
    }
}

交付物:

  • ✅ 钱包系统完整(充值、提现、锁定)
  • ✅ Seata 分布式事务集成
  • ✅ 结算流程验证通过

第 7 周:投票系统 + 定时任务

学习目标: XXL-Job、复杂计算任务、投票算法

核心任务:

1. 投票系统
   - 创意投票
   - 权重计算(粉丝数、历史评分等)
   - 防止刷票

2. XXL-Job 定时任务
   - 拍卖自动结算
   - 热门创意排行榜更新
   - 年度最佳计算
   - 数据报表生成
   - 过期数据清理

投票系统实现:

@Service
@Slf4j
public class VoteService {

    private static final int VOTES_PER_USER = 5;  // 每用户每天5票

    // 用户投票
    @Transactional
    public void vote(Long userId, Long artworkId) {

        // 检查用户今天是否还有投票次数
        String voteKey = "vote:daily:" + userId + ":" +
            LocalDate.now();
        Long votesUsed = redisTemplate.opsForValue()
            .increment(voteKey, 1);

        if (votesUsed > VOTES_PER_USER) {
            throw new VoteLimitException("每天最多投5票");
        }

        // 设置过期时间(明天自动清除)
        if (votesUsed == 1) {
            redisTemplate.expire(voteKey,
                Duration.ofDays(1));
        }

        // 检查用户是否已投过这个作品
        String userVoteKey = "vote:user:" + userId +
            ":artwork:" + artworkId;
        Boolean alreadyVoted = redisTemplate.hasKey(
            userVoteKey
        );

        if (alreadyVoted) {
            throw new BusinessException("已经投过票了");
        }

        // 记录投票
        redisTemplate.opsForValue()
            .set(userVoteKey, "1", Duration.ofDays(365));

        // 增加作品的投票数(使用权重)
        BigDecimal userWeight = calculateUserWeight(userId);
        String artworkVoteKey = "vote:artwork:" + artworkId;
        redisTemplate.opsForZSet()
            .incrementScore(artworkVoteKey,
                userId.toString(),
                userWeight.doubleValue());

        log.info("投票成功:userId={}, artworkId={}",
            userId, artworkId);
    }

    // 计算用户投票权重
    private BigDecimal calculateUserWeight(Long userId) {
        User user = userService.getById(userId);

        // 权重 = 基础分 + 粉丝数加成 + 历史评分加成
        BigDecimal baseScore = BigDecimal.ONE;
        BigDecimal followerBonus = BigDecimal.valueOf(
            user.getFollowerCount() / 100.0  // 100个粉丝 +0.01
        );
        BigDecimal ratingBonus = user.getHistoricalRating()
            .multiply(BigDecimal.valueOf(0.5));

        return baseScore.add(followerBonus).add(ratingBonus);
    }
}

XXL-Job 定时任务配置:

@Configuration
public class XxlJobConfig {

    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        XxlJobSpringExecutor xxlJobSpringExecutor =
            new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(
            "${xxl.job.admin.addresses}"
        );
        xxlJobSpringExecutor.setAppname(
            "${xxl.job.executor.appname}"
        );
        xxlJobSpringExecutor.setIp(
            "${xxl.job.executor.ip}"
        );
        xxlJobSpringExecutor.setPort(
            ${xxl.job.executor.port}
        );
        xxlJobSpringExecutor.setAccessToken(
            "${xxl.job.accessToken}"
        );
        xxlJobSpringExecutor.setLogPath(
            "${xxl.job.executor.logpath}"
        );
        xxlJobSpringExecutor.setLogRetentionDays(
            ${xxl.job.executor.logretentiondays}
        );
        return xxlJobSpringExecutor;
    }
}

具体任务实现:

@Slf4j
@Component
public class AuctionFinalizerJob {

    @Autowired
    private AuctionService auctionService;

    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 每小时执行一次
    @XxlJob("auctionFinalizerJob")
    public void executeJob() {
        log.info("拍卖结算任务开始...");

        try {
            // 查询所有已结束的拍卖
            List<Auction> endedAuctions =
                auctionService.findEndedAuctions();

            for (Auction auction : endedAuctions) {
                // 发送 MQ 事件进行异步结算
                AuctionSettlementEvent event =
                    new AuctionSettlementEvent();
                event.setAuctionId(auction.getId());
                event.setHighestBidder(
                    auction.getHighestBidder()
                );
                event.setHighestBid(
                    auction.getHighestBid()
                );

                rabbitTemplate.convertAndSend(
                    "auction-settlement-exchange",
                    "settlement.key",
                    event
                );
            }

            log.info("拍卖结算任务完成,处理数量:{}",
                endedAuctions.size());

        } catch (Exception e) {
            log.error("拍卖结算任务异常", e);
            throw new RuntimeException(e);
        }
    }
}

@Slf4j
@Component
public class HotArtworkJob {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private ArtworkMapper artworkMapper;

    // 每 2 小时执行一次
    @XxlJob("hotArtworkJob")
    public void executeJob() {
        log.info("热门创意排行榜更新开始...");

        try {
            // 从 Redis 获取最近 24 小时的浏览数据
            Set hotArtworks =
                redisTemplate.opsForZSet()
                    .reverseRange("artwork:views:24h", 0, 99);

            // 保存到数据库(用于展示)
            List<HotArtwork> hotList = new ArrayList<>();
            int rank = 1;
            for (Object artworkId : hotArtworks) {
                HotArtwork hot = new HotArtwork();
                hot.setArtworkId(Long.parseLong(
                    artworkId.toString()
                ));
                hot.setRank(rank++);
                hot.setUpdatedTime(LocalDateTime.now());
                hotList.add(hot);
            }

            hotArtworkMapper.deleteAll();
            hotArtworkMapper.insertBatch(hotList);

            // 更新排行榜缓存
            redisTemplate.opsForValue()
                .set("hot-artworks", hotList,
                    Duration.ofHours(2));

            log.info("热门创意排行榜更新完成");

        } catch (Exception e) {
            log.error("热门创意排行榜更新异常", e);
        }
    }
}

@Slf4j
@Component
public class AnnualAwardCalculationJob {

    @Autowired
    private VoteService voteService;

    @Autowired
    private ArtworkService artworkService;

    // 每年 1 月 1 日凌晨执行
    @XxlJob("annualAwardCalculationJob")
    public void executeJob() {
        log.info("年度最佳创意计算开始...");

        try {
            LocalDateTime startOfYear =
                LocalDateTime.of(
                    LocalDate.now().getYear(), 1, 1,
                    0, 0, 0
                );
            LocalDateTime endOfYear =
                LocalDateTime.now();

            // 查询去年发布的所有作品
            List<Artwork> artworks =
                artworkService.listByCreatedBetween(
                    startOfYear, endOfYear
                );

            // 计算综合得分(投票 + 销售 + 热度)
            Map<Long, BigDecimal> scoreMap = new HashMap<>();
            for (Artwork artwork : artworks) {
                BigDecimal voteScore =
                    voteService.getArtworkVoteScore(
                        artwork.getId()
                    );
                BigDecimal salesScore =
                    artwork.getTotalSales()
                        .divide(BigDecimal.valueOf(1000));
                BigDecimal viewScore =
                    BigDecimal.valueOf(
                        artwork.getViewCount() / 100.0
                    );

                BigDecimal totalScore = voteScore
                    .multiply(BigDecimal.valueOf(0.4))
                    .add(salesScore
                        .multiply(BigDecimal.valueOf(0.35)))
                    .add(viewScore
                        .multiply(BigDecimal.valueOf(0.25)));

                scoreMap.put(artwork.getId(), totalScore);
            }

            // 获取前 10 名
            List<Map.Entry<Long, BigDecimal>> topTen =
                scoreMap.entrySet().stream()
                    .sorted((a, b) ->
                        b.getValue().compareTo(a.getValue())
                    )
                    .limit(10)
                    .collect(Collectors.toList());

            // 保存年度最佳
            AnnualAward award = new AnnualAward();
            award.setYear(LocalDate.now().getYear());
            award.setTopArtworks(topTen.stream()
                .map(Map.Entry::getKey)
                .collect(Collectors.toList()));
            award.setCreatedTime(LocalDateTime.now());
            annualAwardMapper.insert(award);

            log.info("年度最佳创意计算完成:{}", topTen);

        } catch (Exception e) {
            log.error("年度最佳创意计算异常", e);
        }
    }
}

交付物:

  • ✅ 投票系统完整(防刷票、权重计算)
  • ✅ XXL-Job 多个任务实现
  • ✅ 排行榜和年度最佳计算

第 8 周:监控 + 上线部署

学习目标: Skywalking 链路追踪、Docker 部署、性能优化

核心任务:

1. Skywalking 链路追踪
   - 服务间调用可视化
   - 性能瓶颈识别
   - 日志关联

2. Docker 容器化部署
   - 每个微服务编写 Dockerfile
   - 编写 docker-compose.yml
   - 环境变量管理

3. 性能优化
   - 数据库索引优化
   - 缓存热点数据
   - 异步处理优化

Dockerfile 示例:

# 多阶段构建
FROM maven:3.8-openjdk-11 as builder
WORKDIR /app
COPY . .
RUN mvn clean package -DskipTests

# 最终镜像
FROM openjdk:11-jre-slim
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar

# Skywalking 代理
ADD https://archive.apache.org/dist/skywalking/8.9.0/apache-skywalking-apm-8.9.0.tar.gz .
RUN tar -xzf apache-skywalking-apm-8.9.0.tar.gz

ENV JAVA_OPTS="-javaagent:/app/skywalking/agent/skywalking-agent.jar" \
    SW_AGENT_NAME="canvas-chain-service" \
    SW_AGENT_COLLECTOR_BACKEND_SERVICES="skywalking-oap:11800"

EXPOSE 8080
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

docker-compose.yml 完整编排:

version: '3.8'

services:
  # MySQL 数据库
  mysql:
    image: mysql:8.0
    container_name: canvas-mysql
    environment:
      MYSQL_ROOT_PASSWORD: root123456
      MYSQL_DATABASE: canvas_chain
    ports:
      - "3306:3306"
    volumes:
      - ./docs/init.sql:/docker-entrypoint-initdb.d/init.sql
      - mysql-data:/var/lib/mysql
    networks:
      - canvas-network

  # Redis 缓存
  redis:
    image: redis:7-alpine
    container_name: canvas-redis
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes --requirepass canvas123456
    volumes:
      - redis-data:/data
    networks:
      - canvas-network

  # RabbitMQ 消息队列
  rabbitmq:
    image: rabbitmq:3.12-management-alpine
    container_name: canvas-rabbitmq
    environment:
      RABBITMQ_DEFAULT_USER: guest
      RABBITMQ_DEFAULT_PASS: guest
    ports:
      - "5672:5672"
      - "15672:15672"
    volumes:
      - rabbitmq-data:/var/lib/rabbitmq
    networks:
      - canvas-network

  # Nacos 服务注册中心
  nacos:
    image: nacos/nacos-server:v2.2.0
    container_name: canvas-nacos
    environment:
      MODE: standalone
      NACOS_AUTH_ENABLE: "false"
    ports:
      - "8848:8848"
      - "9848:9848"
    volumes:
      - nacos-data:/home/nacos/data
    networks:
      - canvas-network

   # ElasticSearch 搜索引擎
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.5.0
    container_name: canvas-es
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ports:
      - "9200:9200"
    volumes:
      - es-data:/usr/share/elasticsearch/data
    networks:
      - canvas-network

  # Kibana ES 可视化
  kibana:
    image: docker.elastic.co/kibana/kibana:8.5.0
    container_name: canvas-kibana
    environment:
      ELASTICSEARCH_HOSTS: "http://elasticsearch:9200"
    ports:
      - "5601:5601"
    depends_on:
      - elasticsearch
    networks:
      - canvas-network

  # Skywalking OAP 服务器(链路追踪)
  skywalking-oap:
    image: apache/skywalking-oap-server:8.9.0
    container_name: canvas-skywalking-oap
    environment:
      SW_STORAGE: elasticsearch
      SW_ELASTICSEARCH_REPLICAS: 0
      SW_ELASTICSEARCH_PEERS: "elasticsearch:9200"
    ports:
      - "11800:11800"
      - "12800:12800"
    depends_on:
      - elasticsearch
    networks:
      - canvas-network

  # Skywalking UI
  skywalking-ui:
    image: apache/skywalking-ui:8.9.0
    container_name: canvas-skywalking-ui
    ports:
      - "8080:8080"
    environment:
      SW_OAP_ADDRESS: "skywalking-oap:12800"
    depends_on:
      - skywalking-oap
    networks:
      - canvas-network

  # Seata Server 分布式事务协调器
  seata-server:
    image: seataio/seata-server:latest
    container_name: canvas-seata-server
    environment:
      SEATA_PORT: 8091
      SEATA_CONFIG_NAME: file:/seata-config/registry.conf
    ports:
      - "8091:8091"
    volumes:
      - ./config/seata/registry.conf:/seata-config/registry.conf
      - seata-data:/seata-server/sessionStore
    networks:
      - canvas-network

  # XXL-Job 定时任务平台
  xxl-job-admin:
    image: xuxueli/xxl-job-admin:2.4.0
    container_name: canvas-xxl-job-admin
    environment:
      PARAMS: "--server.servlet.context-path=/xxl-job-admin --spring.datasource.url=jdbc:mysql://mysql:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai --spring.datasource.username=root --spring.datasource.password=root123456"
    ports:
      - "8081:8080"
    depends_on:
      - mysql
    networks:
      - canvas-network

  # Sentinel 限流控制台
  sentinel-dashboard:
    image: bladex/sentinel-dashboard:latest
    container_name: canvas-sentinel-dashboard
    ports:
      - "8858:8858"
    networks:
      - canvas-network

  # 网关服务(基于 Spring Cloud Gateway)
  gateway:
    build:
      context: ./canvas-chain-gateway
      dockerfile: Dockerfile
    container_name: canvas-gateway
    environment:
      JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
      SW_AGENT_NAME: "canvas-gateway"
      SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
      NACOS_SERVER_ADDR: "nacos:8848"
      REDIS_HOST: "redis"
      REDIS_PASSWORD: "canvas123456"
    ports:
      - "9000:8080"
    depends_on:
      - nacos
      - redis
      - skywalking-oap
    networks:
      - canvas-network

  # 用户服务
  user-service:
    build:
      context: ./canvas-chain-user-service
      dockerfile: Dockerfile
    container_name: canvas-user-service
    environment:
      JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
      SW_AGENT_NAME: "canvas-user-service"
      SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
      NACOS_SERVER_ADDR: "nacos:8848"
      MYSQL_HOST: "mysql"
      MYSQL_USER: "root"
      MYSQL_PASSWORD: "root123456"
      REDIS_HOST: "redis"
      REDIS_PASSWORD: "canvas123456"
    ports:
      - "8001:8080"
    depends_on:
      - mysql
      - redis
      - nacos
      - skywalking-oap
    networks:
      - canvas-network

  # 创意服务
  artwork-service:
    build:
      context: ./canvas-chain-artwork-service
      dockerfile: Dockerfile
    container_name: canvas-artwork-service
    environment:
      JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
      SW_AGENT_NAME: "canvas-artwork-service"
      SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
      NACOS_SERVER_ADDR: "nacos:8848"
      MYSQL_HOST: "mysql"
      MYSQL_USER: "root"
      MYSQL_PASSWORD: "root123456"
      REDIS_HOST: "redis"
      REDIS_PASSWORD: "canvas123456"
      ELASTICSEARCH_HOSTS: "http://elasticsearch:9200"
    ports:
      - "8002:8080"
    depends_on:
      - mysql
      - redis
      - nacos
      - skywalking-oap
      - elasticsearch
    networks:
      - canvas-network

  # 拍卖服务
  auction-service:
    build:
      context: ./canvas-chain-auction-service
      dockerfile: Dockerfile
    container_name: canvas-auction-service
    environment:
      JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
      SW_AGENT_NAME: "canvas-auction-service"
      SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
      NACOS_SERVER_ADDR: "nacos:8848"
      MYSQL_HOST: "mysql"
      MYSQL_USER: "root"
      MYSQL_PASSWORD: "root123456"
      REDIS_HOST: "redis"
      REDIS_PASSWORD: "canvas123456"
      RABBITMQ_HOST: "rabbitmq"
      RABBITMQ_USER: "guest"
      RABBITMQ_PASSWORD: "guest"
    ports:
      - "8003:8080"
    depends_on:
      - mysql
      - redis
      - nacos
      - rabbitmq
      - skywalking-oap
    networks:
      - canvas-network

  # 盲盒服务
  blindbox-service:
    build:
      context: ./canvas-chain-blindbox-service
      dockerfile: Dockerfile
    container_name: canvas-blindbox-service
    environment:
      JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
      SW_AGENT_NAME: "canvas-blindbox-service"
      SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
      NACOS_SERVER_ADDR: "nacos:8848"
      MYSQL_HOST: "mysql"
      MYSQL_USER: "root"
      MYSQL_PASSWORD: "root123456"
      REDIS_HOST: "redis"
      REDIS_PASSWORD: "canvas123456"
      RABBITMQ_HOST: "rabbitmq"
    ports:
      - "8004:8080"
    depends_on:
      - mysql
      - redis
      - nacos
      - rabbitmq
      - skywalking-oap
    networks:
      - canvas-network

  # 订单服务
  order-service:
    build:
      context: ./canvas-chain-order-service
      dockerfile: Dockerfile
    container_name: canvas-order-service
    environment:
      JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
      SW_AGENT_NAME: "canvas-order-service"
      SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
      NACOS_SERVER_ADDR: "nacos:8848"
      MYSQL_HOST: "mysql"
      MYSQL_USER: "root"
      MYSQL_PASSWORD: "root123456"
      REDIS_HOST: "redis"
      REDIS_PASSWORD: "canvas123456"
      RABBITMQ_HOST: "rabbitmq"
      SEATA_SERVER: "seata-server:8091"
    ports:
      - "8005:8080"
    depends_on:
      - mysql
      - redis
      - nacos
      - rabbitmq
      - skywalking-oap
      - seata-server
    networks:
      - canvas-network

  # 钱包服务
  wallet-service:
    build:
      context: ./canvas-chain-wallet-service
      dockerfile: Dockerfile
    container_name: canvas-wallet-service
    environment:
      JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
      SW_AGENT_NAME: "canvas-wallet-service"
      SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
      NACOS_SERVER_ADDR: "nacos:8848"
      MYSQL_HOST: "mysql"
      MYSQL_USER: "root"
      MYSQL_PASSWORD: "root123456"
      REDIS_HOST: "redis"
      REDIS_PASSWORD: "canvas123456"
      RABBITMQ_HOST: "rabbitmq"
      SEATA_SERVER: "seata-server:8091"
    ports:
      - "8006:8080"
    depends_on:
      - mysql
      - redis
      - nacos
      - rabbitmq
      - skywalking-oap
      - seata-server
    networks:
      - canvas-network

  # 投票服务
  vote-service:
    build:
      context: ./canvas-chain-vote-service
      dockerfile: Dockerfile
    container_name: canvas-vote-service
    environment:
      JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
      SW_AGENT_NAME: "canvas-vote-service"
      SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
      NACOS_SERVER_ADDR: "nacos:8848"
      MYSQL_HOST: "mysql"
      MYSQL_USER: "root"
      MYSQL_PASSWORD: "root123456"
      REDIS_HOST: "redis"
      REDIS_PASSWORD: "canvas123456"
      RABBITMQ_HOST: "rabbitmq"
    ports:
      - "8007:8080"
    depends_on:
      - mysql
      - redis
      - nacos
      - rabbitmq
      - skywalking-oap
    networks:
      - canvas-network

  # NFT 服务
  nft-service:
    build:
      context: ./canvas-chain-nft-service
      dockerfile: Dockerfile
    container_name: canvas-nft-service
    environment:
      JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
      SW_AGENT_NAME: "canvas-nft-service"
      SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
      NACOS_SERVER_ADDR: "nacos:8848"
      MYSQL_HOST: "mysql"
      MYSQL_USER: "root"
      MYSQL_PASSWORD: "root123456"
      REDIS_HOST: "redis"
      REDIS_PASSWORD: "canvas123456"
      RABBITMQ_HOST: "rabbitmq"
    ports:
      - "8008:8080"
    depends_on:
      - mysql
      - redis
      - nacos
      - rabbitmq
      - skywalking-oap
    networks:
      - canvas-network

  # 定时任务服务
  job-service:
    build:
      context: ./canvas-chain-job-service
      dockerfile: Dockerfile
    container_name: canvas-job-service
    environment:
      JAVA_OPTS: "-javaagent:/app/skywalking/agent/skywalking-agent.jar"
      SW_AGENT_NAME: "canvas-job-service"
      SW_AGENT_COLLECTOR_BACKEND_SERVICES: "skywalking-oap:11800"
      NACOS_SERVER_ADDR: "nacos:8848"
      MYSQL_HOST: "mysql"
      MYSQL_USER: "root"
      MYSQL_PASSWORD: "root123456"
      REDIS_HOST: "redis"
      REDIS_PASSWORD: "canvas123456"
      XXL_JOB_ADMIN_ADDRESSES: "http://xxl-job-admin:8080/xxl-job-admin"
    ports:
      - "8009:8080"
    depends_on:
      - mysql
      - redis
      - nacos
      - skywalking-oap
      - xxl-job-admin
    networks:
      - canvas-network

volumes:
  mysql-data:
  redis-data:
  rabbitmq-data:
  nacos-data:
  es-data:
  seata-data:

networks:
  canvas-network:
    driver: bridge

🚀 快速启动指南

# 1. 克隆项目
git clone https://github.com/your-username/canvas-chain.git
cd canvas-chain

# 2. 启动所有容器
docker-compose up -d

# 3. 查看日志
docker-compose logs -f

# 4. 初始化数据库
docker exec canvas-mysql mysql -uroot -proot123456 < docs/init.sql

# 5. 访问各服务
# API 网关: http://localhost:9000
# Nacos: http://localhost:8848/nacos (user/nacos)
# XXL-Job: http://localhost:8081/xxl-job-admin (admin/123456)
# Skywalking: http://localhost:8080
# Kibana: http://localhost:5601
# RabbitMQ: http://localhost:15672 (guest/guest)

📊 性能优化建议

1. 数据库优化

-- 创意表索引优化
CREATE INDEX idx_creator_id ON artwork(creator_id);
CREATE INDEX idx_category ON artwork(category);
CREATE INDEX idx_created_time ON artwork(created_time);
CREATE INDEX idx_status ON artwork(status);

-- 拍卖表索引
CREATE INDEX idx_auction_status ON auction(status);
CREATE INDEX idx_auction_end_time ON auction(end_time);

-- 交易表索引
CREATE INDEX idx_transaction_user ON transaction(user_id);
CREATE INDEX idx_transaction_created ON transaction(created_time);

2. Redis 热点数据

@Service
public class CacheWarmupService {

    @Scheduled(cron = "0 0 * * * *")  // 每小时执行
    public void warmupHotData() {
        // 预热热门创意
        List<Artwork> hotArtworks = artworkService
            .listHotArtworks();
        for (Artwork artwork : hotArtworks) {
            String key = "artwork:" + artwork.getId();
            redisTemplate.opsForValue()
                .set(key, artwork, Duration.ofHours(1));
        }

        // 预热排行榜
        redisTemplate.opsForValue()
            .set("ranking:daily", getRankingData(),
                Duration.ofHours(1));
    }
}

3. 异步处理优化

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "taskExecutor")
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor =
            new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(1000);
        executor.setThreadNamePrefix("canvas-async-");
        executor.initialize();
        return executor;
    }
}

// 使用
@Service
public class AuctionService {

    @Async("taskExecutor")
    public void processAuctionSettlement(Long auctionId) {
        // 耗时操作
    }
}

📈 监控指标

关键指标监控

@Component
@Slf4j
public class MetricsCollector {

    @Autowired
    private MeterRegistry meterRegistry;

    // 记录拍卖竞价
    public void recordBid(Long auctionId,
                         BigDecimal bidAmount) {
        meterRegistry.counter(
            "auction.bid.count",
            "auctionId", auctionId.toString()
        ).increment();

        meterRegistry.gauge(
            "auction.bid.amount",
            bidAmount.doubleValue()
        );
    }

    // 记录交易
    public void recordTransaction(Long userId,
                                 BigDecimal amount) {
        meterRegistry.counter(
            "transaction.count",
            "userId", userId.toString()
        ).increment();

        meterRegistry.timer("transaction.duration")
            .record(Duration.ofSeconds(1));
    }

    // 记录缓存命中率
    public void recordCacheHit(String cacheName,
                              boolean hit) {
        meterRegistry.counter(
            "cache.hits",
            "cache", cacheName,
            "hit", String.valueOf(hit)
        ).increment();
    }
}

🎯 最终交付清单

8周后你将得到:

阶段 (Week) 完成内容 (Deliverables) 技术栈 (Tech Stack)
W1 微服务架构搭建:项目脚手架、服务注册与发现、数据库初始化 Nacos、Spring Cloud Alibaba、Spring Boot、MySQL
W2 创意服务开发 + 多级缓存:作品 CRUD、缓存穿透/击穿防护、搜索引擎接入 Redis、Caffeine、本地缓存、布隆过滤器、ElasticSearch
W3 消息队列系统:事件驱动设计、死信队列、消息幂等性保证 RabbitMQ、Spring AMQP、消息模型设计
W4 拍卖核心逻辑:实时竞价、限流、原子性出价脚本 WebSocket、Sentinel、Redis Lua、并发锁
W5 盲盒系统 + 订单管理:加权抽奖、库存管理、订单全流程 抽奖算法、幂等设计、订单状态机、事务管理
W6 钱包系统 + 分布式事务:扣费、锁定、收益结算、跨服务一致性 Seata、TCC 模式、AT 模式、补偿机制
W7 投票系统 + 定时任务:权重投票、排行榜、自动化任务调度 XXL-Job、投票算法、排行榜计算
W8 监控 + 上线部署:链路追踪、容器化、自动化部署流程 SkyWalking、Docker、docker-compose、CI/CD

代码统计:

  • ✅ 总代码行数:~50,000+ 行
  • ✅ 微服务数量:10 个
  • ✅ 数据库表:30+ 张
  • ✅ API 接口:200+ 个
  • ✅ 中间件集成:全栈

简历亮点:

  • ✅ 完整的分布式系统设计经验
  • ✅ 中间件全栈掌握(MQ、缓存、注册中心、限流、事务、任务调度)
  • ✅ 高并发场景实战经验
  • ✅ 企业级代码规范和架构设计能力
  • ✅ 可部署的生产级系统

VibeCoding 导航:⬅️ 02-CanvasChain 第1周 - 基础架构搭建详细设计文档 | 01-计划 | ➡️ 02-CanvasChain 项目 8 周计划可行性分析